<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    高可用的Redis主从复制集群，从理论到实践.md |  
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-deploy/高可用的Redis主从复制集群，从理论到实践"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  高可用的Redis主从复制集群，从理论到实践.md
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/deploy/%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84Redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%9B%86%E7%BE%A4%EF%BC%8C%E4%BB%8E%E7%90%86%E8%AE%BA%E5%88%B0%E5%AE%9E%E8%B7%B5/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/deploy/">deploy</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">6.8k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">32 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <h2 id="高可用的Redis主从复制集群，从理论到实践"><a href="#高可用的Redis主从复制集群，从理论到实践" class="headerlink" title="高可用的Redis主从复制集群，从理论到实践"></a>高可用的Redis主从复制集群，从理论到实践</h2><p><a href="javascript:void(0);">Java知音</a> <em>3天前</em></p>
<p><img src="http://iubest.gitee.io/pic/640-1601002200465.gif" alt="img"></p>
<p><em>作者**：Sicimike</em></p>
<p><em>blog.csdn.net/Baisitao_/article/details/105545410</em></p>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>我们都知道，服务如果只部署一个节点，很容易出现单点故障，从而导致服务不可用。所以服务做成高可用是很有必要的，Redis服务自然也是这样。</p>
<p>本文主要从理论和实践两方面讲解Redis高可用集群。手把手教你搭建一个高可用的redis主从复制集群。</p>
<p>本文采取理论和实践穿插讲解的方式，如果只关心集群的搭建，可以跳过文中理论部分。</p>
<h2 id="前置阅读"><a href="#前置阅读" class="headerlink" title="前置阅读"></a>前置阅读</h2><ul>
<li><p>Redis持久化：</p>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/Baisitao_/article/details/105461153">https://blog.csdn.net/Baisitao_/article/details/105461153</a></p>
</li>
</ul>
<h3 id="实验环境"><a href="#实验环境" class="headerlink" title="实验环境"></a>实验环境</h3><ul>
<li>VMware Workstation 15</li>
<li>CentOS Linux release 7.7.1908</li>
<li>Redis-5.0.8</li>
</ul>
<h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ul>
<li>三个节点ip分别为<code>192.168.1.101</code>、<code>192.168.1.102</code>、<code>192.168.1.103</code></li>
<li>确保三个节点都能访问互联网，并且三个节点能够相互通信</li>
<li>确保Linux的<code>yum</code>、<code>wget</code>、<code>tar</code>、<code>gcc</code>等基础命令、编译器可用</li>
<li>建议先关闭防火墙，Centos 7操作如下</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --state ## 查看防火墙状态 not running表示已经关闭</span><br><span class="line">systemctl stop firewalld.service ## 关闭防火墙</span><br><span class="line">systemctl disable firewalld.service ## 禁止开机启动防火墙</span><br></pre></td></tr></table></figure>

<h2 id="redis单机安装"><a href="#redis单机安装" class="headerlink" title="redis单机安装"></a>redis单机安装</h2><p>下载</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget http:&#x2F;&#x2F;download.redis.io&#x2F;releases&#x2F;redis-5.0.8.tar.gz</span><br></pre></td></tr></table></figure>

<p>解压</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf redis-5.0.8.tar.gz</span><br></pre></td></tr></table></figure>

<p>编译</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd redis-5.0.8</span><br><span class="line">make</span><br></pre></td></tr></table></figure>

<p>安装</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">make install  ## 或者指定安装目录 make install PREFIX&#x3D;指定路径。默认路径是&#x2F;usr&#x2F;local&#x2F;bin</span><br><span class="line">.&#x2F;utils&#x2F;install_server.sh  ## 安装成服务，如果上一步配置了PREFIX，需要把安装路径配置到环境变量&#x2F;etc&#x2F;profile</span><br></pre></td></tr></table></figure>

<p><code>install_server.sh</code>是redis提供的脚本，运行之后会让你指定几个配置：端口号、配置文件路径、日志文件路径、数据文件路径。</p>
<p>如果都设置成默认值，redis根据按照端口号来区分同一台主机上的不同的实例，因为<code>install_server.sh</code>可以多次运行，每次运行相当于安装了一个实例。</p>
<p>安装过程如果都是默认安装，会有以下几个配置：</p>
<ul>
<li>端口号：<code>6379</code></li>
<li>配置文件路径：<code>/etc/redis/6379.conf</code></li>
<li>日志文件路径：<code>/var/log/redis_6379.log</code></li>
<li>数据文件路径：<code>/var/lib/redis/6379/</code></li>
<li><code>redis-server.sh</code>路径：<code>/usr/local/bin/</code></li>
<li><code>redis-cli.sh</code>路径：<code>/usr/local/bin/</code></li>
</ul>
<p>安装成功会出现如下日志</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Copied &#x2F;tmp&#x2F;6379.conf &#x3D;&gt; &#x2F;etc&#x2F;init.d&#x2F;redis_6379</span><br><span class="line">Installing service...</span><br><span class="line">Successfully added to chkconfig!</span><br><span class="line">Successfully added to runlevels 345!</span><br><span class="line">Starting Redis server...</span><br><span class="line">Installation successful!</span><br></pre></td></tr></table></figure>

<p>可以看到redis服务已经自动启动。</p>
<h2 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h2><p>Redis主从复制是redis3.0之后自带的一种集群实现方式，不需要其他的中间件。是一种基于异步复制的主从实现方式。所以Redis主从复制并不能保证数据的强一致性.。集群在特定的条件下可能会丢失写操作。</p>
<h3 id="集群结构"><a href="#集群结构" class="headerlink" title="集群结构"></a>集群结构</h3><p>现在来搭建一个一主两从的集群，集群拓扑图如下<br><img src="http://iubest.gitee.io/pic/640-1601002200470.webp" alt="img"></p>
<p>其中<code>master</code>节点可写可读，一般用来处理写请求，<code>slave</code>节点默认情况下是只读的，所以用来处理读请求。两个<code>slave</code>节点的数据都是从<code>master</code>节点复制过去的。所以这种集群也叫读写分离。</p>
<h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>redis配置文件默认路径为<code>/etc/redis/6379.conf</code>，用vi/vim打开，三个节点都配置如下内容</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">## 需要绑定的ip地址</span><br><span class="line">bind 127.0.0.1 192.168.1.101 192.168.1.102 192.168.1.103</span><br><span class="line"></span><br><span class="line">## 关闭后台运行，便于观察</span><br><span class="line">daemonize no</span><br><span class="line"></span><br><span class="line">## 注释日志路径，让日志直接输出在控制台，便于观察</span><br><span class="line"># logfile &#x2F;var&#x2F;log&#x2F;redis_6379.log</span><br><span class="line"></span><br><span class="line">## 关闭AOF持久化模式</span><br><span class="line">appendonly no</span><br></pre></td></tr></table></figure>

<h3 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h3><p>配置完成后分别启动三个节点</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd &#x2F;usr&#x2F;local&#x2F;bin</span><br><span class="line">redis-server &#x2F;etc&#x2F;redis&#x2F;6379.conf</span><br></pre></td></tr></table></figure>

<h3 id="设置主从关系"><a href="#设置主从关系" class="headerlink" title="设置主从关系"></a>设置主从关系</h3><p>两个<code>slave</code>节点用redis-cli客户端连接redis-server后，均执行如下命令，把自己设置成<code>master</code>节点的<code>slave</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">replicaof 192.168.1.101 6379</span><br></pre></td></tr></table></figure>

<p><code>replicaof</code>也可以直接写在配置文件中（文中为了实验效果，以命令的方式执行）</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">################################# REPLICATION #################################</span><br><span class="line"></span><br><span class="line"># Master-Replica replication. Use replicaof to make a Redis instance a copy of</span><br><span class="line"># another Redis server. A few things to understand ASAP about Redis replication.</span><br><span class="line">#</span><br><span class="line">#   +------------------+      +---------------+</span><br><span class="line">#   |      Master      | ---&gt; |    Replica    |</span><br><span class="line">#   | (receive writes) |      |  (exact copy) |</span><br><span class="line">#   +------------------+      +---------------+</span><br><span class="line">#</span><br><span class="line"># 1) Redis replication is asynchronous, but you can configure a master to</span><br><span class="line">#    stop accepting writes if it appears to be not connected with at least</span><br><span class="line">#    a given number of replicas.</span><br><span class="line"># 2) Redis replicas are able to perform a partial resynchronization with the</span><br><span class="line">#    master if the replication link is lost for a relatively small amount of</span><br><span class="line">#    time. You may want to configure the replication backlog size (see the next</span><br><span class="line">#    sections of this file) with a sensible value depending on your needs.</span><br><span class="line"># 3) Replication is automatic and does not need user intervention. After a</span><br><span class="line">#    network partition replicas automatically try to reconnect to masters</span><br><span class="line">#    and resynchronize with them.</span><br><span class="line">#</span><br><span class="line"># replicaof &lt;masterip&gt; &lt;masterport&gt;</span><br></pre></td></tr></table></figure>

<p><code>replicaof</code>在redis5.0之前的版本叫<code>slaveof</code>，命令描述如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; help slaveof</span><br><span class="line"></span><br><span class="line">  SLAVEOF host port</span><br><span class="line">  summary: Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.</span><br><span class="line">  since: 1.0.0</span><br><span class="line">  group: server</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; help replicaof</span><br><span class="line"></span><br><span class="line">  REPLICAOF host port</span><br><span class="line">  summary: Make the server a replica of another instance, or promote it as master.</span><br><span class="line">  since: 5.0.0</span><br><span class="line">  group: server</span><br></pre></td></tr></table></figure>

<p>命令成功执行后<code>192.168.1.101（master）</code>会出现如下日志</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">1817:M 16 Apr 2020 22:33:36.802 * Replica 192.168.1.102:6379 asks for synchronization</span><br><span class="line">1817:M 16 Apr 2020 22:33:36.802 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for &#39;e801c600a0a2381a65e1aec22daba7db82cb02f8&#39;, my replication IDs are &#39;be75572b8e6624da4971aa16448600c9822fd42a&#39; and &#39;0000000000000000000000000000000000000000&#39;)</span><br><span class="line">1817:M 16 Apr 2020 22:33:36.803 * Starting BGSAVE for SYNC with target: disk</span><br><span class="line">1817:M 16 Apr 2020 22:33:36.837 * Background saving started by pid 1822</span><br><span class="line">1822:C 16 Apr 2020 22:33:36.944 * DB saved on disk</span><br><span class="line">1822:C 16 Apr 2020 22:33:36.944 * RDB: 6 MB of memory used by copy-on-write</span><br><span class="line">1817:M 16 Apr 2020 22:33:37.038 * Background saving terminated with success</span><br><span class="line">1817:M 16 Apr 2020 22:33:37.038 * Synchronization with replica 192.168.1.102:6379 succeeded</span><br></pre></td></tr></table></figure>

<p>我们逐行看一下<code>192.168.1.101（master）</code>做了哪些事。</p>
<ul>
<li>第一行意思是有一个<code>salve</code>节点<code>192.168.1.102:6379</code>请求同步</li>
<li>第二行意思是会进行全量同步，因为是第一次请求同步</li>
<li>第三行意思是开始执行<code>BGSAVE</code>把数据持久化到磁盘</li>
<li>第四行意思是pid为1822子进程开始执行持久化</li>
<li>第五行意思是持久化完成</li>
<li>第六行意思是<code>copy-on-write</code>机制使用了6M内存</li>
</ul>
<p>最后两行表示同步过程已经完成。<code>master</code>节点是把数据以RDB的形式持久化到磁盘，再通过网络发送给<code>slave</code>。参数<code>repl-diskless-sync</code>设置成<code>no</code>的话，表示数据不经过磁盘，直接发送给<code>slave</code>。</p>
<p>看了<code>192.168.1.101（master）</code>的日志，再来看<code>salve</code>的日志，任取一个<code>slave</code>的日志</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">2013:S 16 Apr 2020 22:33:36.233 * Before turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.233 * REPLICAOF 192.168.1.101:6379 enabled (user request from &#39;id&#x3D;3 addr&#x3D;127.0.0.1:33550 fd&#x3D;8 name&#x3D; age&#x3D;4 idle&#x3D;0 flags&#x3D;N db&#x3D;0 sub&#x3D;0 psub&#x3D;0 multi&#x3D;-1 qbuf&#x3D;49 qbuf-free&#x3D;32719 obl&#x3D;0 oll&#x3D;0 omem&#x3D;0 events&#x3D;r cmd&#x3D;replicaof&#39;)</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.808 * Connecting to MASTER 192.168.1.101:6379</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.808 * MASTER &lt;-&gt; REPLICA sync started</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.809 * Non blocking connect for SYNC fired the event.</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.810 * Master replied to PING, replication can continue...</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.811 * Trying a partial resynchronization (request e801c600a0a2381a65e1aec22daba7db82cb02f8:1).</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.946 * Full resync from master: a9861cdcfdb3358ea0a3bb5a4df2895938c1c2d0:0</span><br><span class="line">2013:S 16 Apr 2020 22:33:36.946 * Discarding previously cached master state.</span><br><span class="line">2013:S 16 Apr 2020 22:33:37.048 * MASTER &lt;-&gt; REPLICA sync: receiving 175 bytes from master</span><br><span class="line">2013:S 16 Apr 2020 22:33:37.048 * MASTER &lt;-&gt; REPLICA sync: Flushing old data</span><br><span class="line">2013:S 16 Apr 2020 22:33:37.048 * MASTER &lt;-&gt; REPLICA sync: Loading DB in memory</span><br><span class="line">2013:S 16 Apr 2020 22:33:37.048 * MASTER &lt;-&gt; REPLICA sync: Finished with success</span><br></pre></td></tr></table></figure>

<p><code>salve</code>节点日志较多，告诉我们具体做了这些事</p>
<ul>
<li>向<code>192.168.1.101:6379(master)</code>请求同步</li>
<li>发送指令<code>SYNC</code></li>
<li>收到<code>master</code>的回复</li>
<li>全量同步，收到了175 bytes</li>
<li>清空自身的数据(<code>Flushing old data</code>)</li>
<li>加载<code>master</code>传送的数据到内存(<code>Loading DB in memory</code>)</li>
</ul>
<p>结合<code>master</code>和<code>slave</code>日志，可以看出复制的大致过程。</p>
<p>完整的主从复制的过程如下</p>
<p><img src="http://iubest.gitee.io/pic/640-1601002200466.webp" alt="img">redis主从复制</p>
<p><code>master</code>收到某个<code>slave</code>第一次请求的同步时，会进行全量同步，在同步期间会把执行过的修改数据的命令写入缓存，等同步完成后，再发送给<code>slave</code>节点执行。第一次全量同步完成后，<code>master</code>会持续给<code>slave</code>节点发送写命令，以保证主从节点数据一致性。</p>
<p>在这里可以思考一个问题，<code>slave</code>节点在全量同步的这段时间中，里面的数据能不能被客户端查询呢？</p>
<p><code>replicaof-server-stale-data</code>参数设置成<code>yes</code>表示可以查，设置成<code>no</code>表示同步必须完成才能查。</p>
<h3 id="操作"><a href="#操作" class="headerlink" title="操作"></a>操作</h3><p>先往<code>master</code>节点写入数据</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">192.168.1.101:6379&gt; set key1 hello</span><br><span class="line">OK</span><br></pre></td></tr></table></figure>

<p>再从<code>slave</code>节点获取（注意提示符中的ip地址），毫无疑问是可以获取的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">192.168.1.102:6379&gt; get key1</span><br><span class="line">&quot;hello&quot;</span><br></pre></td></tr></table></figure>

<p>如果往<code>slave</code>节点写入数据会怎样？</p>
<p>默认情况下slave节点禁止写入，所以会报错。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">192.168.1.102:6379&gt; set key2 world</span><br><span class="line">(error) READONLY You can&#39;t write against a read only replica.</span><br></pre></td></tr></table></figure>

<p><code>replica-read-only</code>参数可以设置<code>slave</code>允许写入</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"># You can configure a replica instance to accept writes or not. Writing against</span><br><span class="line"># a replica instance may be useful to store some ephemeral data (because data</span><br><span class="line"># written on a replica will be easily deleted after resync with the master) but</span><br><span class="line"># may also cause problems if clients are writing to it because of a</span><br><span class="line"># misconfiguration.</span><br><span class="line">#</span><br><span class="line"># Since Redis 2.6 by default replicas are read-only.</span><br><span class="line">#</span><br><span class="line"># Note: read only replicas are not designed to be exposed to untrusted clients</span><br><span class="line"># on the internet. It&#39;s just a protection layer against misuse of the instance.</span><br><span class="line"># Still a read only replica exports by default all the administrative commands</span><br><span class="line"># such as CONFIG, DEBUG, and so forth. To a limited extent you can improve</span><br><span class="line"># security of read only replicas using &#39;rename-command&#39; to shadow all the</span><br><span class="line"># administrative &#x2F; dangerous commands.</span><br><span class="line">replica-read-only yes</span><br></pre></td></tr></table></figure>

<p>至此，最简单的主从复制集群已经搭建完成。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601002200536.webp" alt="img"></p>
<h2 id="故障"><a href="#故障" class="headerlink" title="故障"></a>故障</h2><p>你已经是一个成熟的程序员了，应该要学会面向故障编程。</p>
<p>在这个集群中有三个节点，两种角色。<code>salve</code>可能会挂，<code>master</code>也可能会挂。我们先看下<code>salve</code>节点挂了会怎样。</p>
<h3 id="slave故障"><a href="#slave故障" class="headerlink" title="slave故障"></a>slave故障</h3><p>首先让一台<code>slave</code>宕机，由于配置了2个<code>slave</code>节点，所以一个出了故障，不至于整个服务不可用。只要尽快处理故障，恢复<code>slave</code>即可，实验步骤如下。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601002200538.webp" alt="img"></p>
<p>现在重启出故障的<code>slave</code>节点</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;usr&#x2F;local&#x2F;bin&#x2F;redis-server &#x2F;etc&#x2F;redis&#x2F;6379.conf --replicaof 192.168.1.101 6379</span><br></pre></td></tr></table></figure>

<p>观察<code>master</code>，会打印如下日志信息</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">2168:M 17 Apr 2020 13:38:16.282 * Replica 192.168.1.102:6379 asks for synchronization</span><br><span class="line">2168:M 17 Apr 2020 13:38:16.282 * Partial resynchronization request from 192.168.1.102:6379 accepted. Sending 143 bytes of backlog starting from offset 1473.</span><br></pre></td></tr></table></figure>

<p>可以看到只打印了2行日志。表示收到了<code>192.168.1.102:6379（slave）</code>节点的同步请求，并且接受同步，从偏移（<code>offset</code>）1473开始传输，共传输了143 bytes。这意味着<code>slave</code>的重新连接，并没有触发全量同步，而是增量同步。同步的数据只是故障期间在<code>master</code>写入的那部分数据。</p>
<p>上面的操作是没有开启<code>AOF</code>的情况，如果开启<code>AOF</code>，情况又不一样。下面来操作开启<code>AOF</code>的情况，操作步骤和上面一样，区别仅仅是<code>slave</code>节点重启时开启<code>AOF</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;usr&#x2F;local&#x2F;bin&#x2F;redis-server &#x2F;etc&#x2F;redis&#x2F;6379.conf --replicaof 192.168.1.101 6379 --appendonly yes</span><br></pre></td></tr></table></figure>

<p>观察<code>master</code>节点，可以看到如下日志</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">2168:M 17 Apr 2020 13:45:21.977 * Replica 192.168.1.102:6379 asks for synchronization</span><br><span class="line">2168:M 17 Apr 2020 13:45:21.977 * Full resync requested by replica 192.168.1.102:6379</span><br><span class="line">2168:M 17 Apr 2020 13:45:21.977 * Starting BGSAVE for SYNC with target: disk</span><br><span class="line">2168:M 17 Apr 2020 13:45:21.978 * Background saving started by pid 2306</span><br><span class="line">2306:C 17 Apr 2020 13:45:22.009 * DB saved on disk</span><br><span class="line">2306:C 17 Apr 2020 13:45:22.010 * RDB: 8 MB of memory used by copy-on-write</span><br><span class="line">2168:M 17 Apr 2020 13:45:22.111 * Background saving terminated with success</span><br><span class="line">2168:M 17 Apr 2020 13:45:22.111 * Synchronization with replica 192.168.1.102:6379 succeeded</span><br></pre></td></tr></table></figure>

<p>根据日志可以看出，<code>slave</code>节点重启时如果开启了<code>AOF</code>，会触发全量同步。即使整个实验一开始就把所以节点都开启<code>AOF</code>，这里也会触发全量同步。</p>
<p>下面是<code>slave</code>日志，也可以证明触发了全量同步。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">2598:S 17 Apr 2020 13:45:21.967 * Ready to accept connections</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.968 * Connecting to MASTER 192.168.1.101:6379</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.968 * MASTER &lt;-&gt; REPLICA sync started</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.969 * Non blocking connect for SYNC fired the event.</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.971 * Master replied to PING, replication can continue...</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.973 * Partial resynchronization not possible (no cached master)</span><br><span class="line">2598:S 17 Apr 2020 13:45:21.977 * Full resync from master: 8b57ea32e3bada6e91d3f371123cb693df2eec8b:2235</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.107 * MASTER &lt;-&gt; REPLICA sync: receiving 271 bytes from master</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.108 * MASTER &lt;-&gt; REPLICA sync: Flushing old data</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.122 * MASTER &lt;-&gt; REPLICA sync: Loading DB in memory</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.122 * MASTER &lt;-&gt; REPLICA sync: Finished with success</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.125 * Background append only file rewriting started by pid 2602</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.178 * AOF rewrite child asks to stop sending diffs.</span><br><span class="line">2602:C 17 Apr 2020 13:45:22.179 * Parent agreed to stop sending diffs. Finalizing AOF...</span><br><span class="line">2602:C 17 Apr 2020 13:45:22.179 * Concatenating 0.00 MB of AOF diff received from parent.</span><br><span class="line">2602:C 17 Apr 2020 13:45:22.179 * SYNC append only file rewrite performed</span><br><span class="line">2602:C 17 Apr 2020 13:45:22.180 * AOF rewrite: 4 MB of memory used by copy-on-write</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.274 * Background AOF rewrite terminated with success</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.274 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)</span><br><span class="line">2598:S 17 Apr 2020 13:45:22.275 * Background AOF rewrite finished successfully</span><br></pre></td></tr></table></figure>

<h3 id="master故障"><a href="#master故障" class="headerlink" title="master故障"></a>master故障</h3><p>由于在这个集群中，<code>master</code>节点只有一个，万一宕机了，整个服务就无法写入数据了，相当于服务不可用。这个时候救世主就出现了。哦，不，是哨兵（Sentinel）出现了。</p>
<p>Redis Sentinel（哨兵）是Redis官方的高可用性解决方案，用于管理多个 Redis 服务器（instance），哨兵的作用主要有三个：</p>
<ul>
<li>监控（Monitoring）：Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。</li>
<li>提醒（Notification）：当被监控的某个 Redis 服务器出现问题时， Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。</li>
<li>自动故障迁移（Automatic failover）：当一个主服务器不能正常工作时，Sentinel 会开始一次自动故障迁移操作， 它会将失效主服务器（<code>master</code>）的其中一个从服务器（<code>slave</code>）升级为新的主服务器（<code>master</code>），并让失效主服务器的其他从服务器改为复制新的主服务器；当客户端试图连接失效的主服务器时，集群也会向客户端返回新主服务器的地址， 使得集群可以使用新主服务器代替失效服务器。</li>
</ul>
<p>如果单单只是一个哨兵实例来监控集群，那哨兵必定也存在单点故障的问题，所以需要多个哨兵实例。加入哨兵后的集群结构如下</p>
<p><img src="http://iubest.gitee.io/pic/640-1601002200307.webp" alt="img">redis集群</p>
<p>26379是<code>sentinel</code>的默认端口，三个哨兵分别放在三个节点上。</p>
<h3 id="哨兵"><a href="#哨兵" class="headerlink" title="哨兵"></a>哨兵</h3><p>redis安装包的解压目录下会有一个<code>sentinel.conf</code>文件，这就是哨兵的配置文件，为了方便，把它拷贝到和redis配置文件相同的目录</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">## 拷贝哨兵配置文件</span><br><span class="line">cp sentinel.conf &#x2F;etc&#x2F;redis&#x2F;</span><br><span class="line">## 配置哨兵的配置文件</span><br><span class="line">vim &#x2F;etc&#x2F;redis&#x2F;sentinel.conf</span><br></pre></td></tr></table></figure>

<p>需要改的地方只有一个，就是指定哨兵要监控哪个<code>master</code>，因为<code>master</code>是可以知道有哪些<code>slave</code>节点连接了自己，所以监控<code>master</code>就够了。注意三个<code>sentinel</code>节点都是配置<code>master</code>的ip和端口</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"># sentinel monitor &lt;master-name&gt; &lt;ip&gt; &lt;redis-port&gt; &lt;quorum&gt;</span><br><span class="line">#</span><br><span class="line"># Tells Sentinel to monitor this master, and to consider it in O_DOWN</span><br><span class="line"># (Objectively Down) state only if at least &lt;quorum&gt; sentinels agree.</span><br><span class="line">#</span><br><span class="line"># Note that whatever is the ODOWN quorum, a Sentinel will require to</span><br><span class="line"># be elected by the majority of the known Sentinels in order to</span><br><span class="line"># start a failover, so no failover can be performed in minority.</span><br><span class="line">#</span><br><span class="line"># Replicas are auto-discovered, so you don&#39;t need to specify replicas in</span><br><span class="line"># any way. Sentinel itself will rewrite this configuration file adding</span><br><span class="line"># the replicas using additional configuration options.</span><br><span class="line"># Also note that the configuration file is rewritten when a</span><br><span class="line"># replica is promoted to master.</span><br><span class="line">#</span><br><span class="line"># Note: master name should not include special characters or spaces.</span><br><span class="line"># The valid charset is A-z 0-9 and the three characters &quot;.-_&quot;.</span><br><span class="line">sentinel monitor mymaster 192.168.1.101 6379 2</span><br></pre></td></tr></table></figure>

<p>配置指示 Sentinel 去监视一个名为<code>mymaster</code>的主服务器， 这个主服务器的 IP 地址为 192.168.1.101 ， 端口号为 6379 。后面那个2表示这个主服务器判断为失效至少需要 2 个 Sentinel 同意 （只要同意 Sentinel 的数量不达标，自动故障迁移就不会执行）。</p>
<p>搜索Java知音公众号，回复“后端面试”，送你一份Java面试题宝典.pdf</p>
<blockquote>
<p>不过要注意， 无论设置要多少个 Sentinel 同意才能判断一个服务器失效， 一个 Sentinel 都需要获得系统中多数（majority） Sentinel 的支持， 才能发起一次自动故障迁移</p>
</blockquote>
<p>正是为了更好的区分多数和少数，所以一般使用奇数个sentinel实例来监控集群。</p>
<p>配置文件修改完成后，开始启动三个哨兵，哨兵的启动有两种方式：直接运行<code>redis-sentinel</code>、运行<code>redis-server --sentinel</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-server &#x2F;etc&#x2F;redis&#x2F;sentinel.conf --sentinel</span><br></pre></td></tr></table></figure>

<p>第一个哨兵启动日志如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">2873:X 17 Apr 2020 20:56:54.495 # WARNING: The TCP backlog setting of 511 cannot be enforced because &#x2F;proc&#x2F;sys&#x2F;net&#x2F;core&#x2F;somaxconn is set to the lower value of 128.</span><br><span class="line">2873:X 17 Apr 2020 20:56:54.498 # Sentinel ID is 643817dcf5ba6d53a737782a75706a62df869e33</span><br><span class="line">2873:X 17 Apr 2020 20:56:54.498 # +monitor master mymaster 192.168.1.101 6379 quorum 2</span><br><span class="line">2873:X 17 Apr 2020 20:56:54.500 * +slave slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2873:X 17 Apr 2020 20:56:54.503 * +slave slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br></pre></td></tr></table></figure>

<p>可以看到哨兵打印出了自己的ID，还监控了<code>192.168.1.101 6379（master）</code>和两个<code>slave</code>节点</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">3031:X 17 Apr 2020 20:59:59.153 # WARNING: The TCP backlog setting of 511 cannot be enforced because &#x2F;proc&#x2F;sys&#x2F;net&#x2F;core&#x2F;somaxconn is set to the lower value of 128.</span><br><span class="line">3031:X 17 Apr 2020 20:59:59.158 # Sentinel ID is e784d728f7a813de688ea800a88bda6aca0512ff</span><br><span class="line">3031:X 17 Apr 2020 20:59:59.158 # +monitor master mymaster 192.168.1.101 6379 quorum 2</span><br><span class="line">3031:X 17 Apr 2020 20:59:59.164 * +slave slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">3031:X 17 Apr 2020 20:59:59.166 * +slave slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">3031:X 17 Apr 2020 21:00:00.115 * +sentinel sentinel 643817dcf5ba6d53a737782a75706a62df869e33 192.168.1.101 26379 @ mymaster 192.168.1.101 6379</span><br></pre></td></tr></table></figure>

<p>启动第二个哨兵时，也打印了同样的日志。除此之外，还多打印了一行关于sentinel的日志。可以看出打印出的sentinel的ID就是第一个哨兵的。也就是说哨兵在监控<code>master</code>的时候，除了可以发下<code>slave</code>节点，还可以发现监控<code>master</code>节点的其他哨兵。回头再看第一个哨兵的日志，也会多打印一行，就是第二个哨兵的ID。</p>
<p>三个哨兵已经准备就绪，接下来再让<code>master</code>宕机。</p>
<p><code>master</code>宕机30秒后，Sentinel 认为服务器已经宕机，由参数<code>sentinel down-after-milliseconds</code>指定</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># sentinel down-after-milliseconds &lt;master-name&gt; &lt;milliseconds&gt;</span><br><span class="line">#</span><br><span class="line"># Number of milliseconds the master (or any attached replica or sentinel) should</span><br><span class="line"># be unreachable (as in, not acceptable reply to PING, continuously, for the</span><br><span class="line"># specified period) in order to consider it in S_DOWN state (Subjectively</span><br><span class="line"># Down).</span><br><span class="line">#</span><br><span class="line"># Default is 30 seconds.</span><br></pre></td></tr></table></figure>

<p>超过半数的Sentinel感知到<code>master</code>宕机后会进行投票选举，从剩下的两个<code>slave</code>中选出一个<code>master</code>。三个哨兵日志分别如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">2873:X 17 Apr 2020 21:02:57.687 # +sdown master mymaster 192.168.1.101 6379</span><br><span class="line">2873:X 17 Apr 2020 21:02:57.765 # +new-epoch 1</span><br><span class="line">2873:X 17 Apr 2020 21:02:57.766 # +vote-for-leader a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 1</span><br><span class="line">2873:X 17 Apr 2020 21:02:58.326 # +config-update-from sentinel a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 192.168.1.103 26379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2873:X 17 Apr 2020 21:02:58.326 # +switch-master mymaster 192.168.1.101 6379 192.168.1.103 6379</span><br><span class="line">2873:X 17 Apr 2020 21:02:58.327 * +slave slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">2873:X 17 Apr 2020 21:02:58.327 * +slave slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">2873:X 17 Apr 2020 21:03:28.343 # +sdown slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">3031:X 17 Apr 2020 21:02:57.686 # +sdown master mymaster 192.168.1.101 6379</span><br><span class="line">3031:X 17 Apr 2020 21:02:57.743 # +new-epoch 1</span><br><span class="line">3031:X 17 Apr 2020 21:02:57.745 # +vote-for-leader a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 1</span><br><span class="line">3031:X 17 Apr 2020 21:02:57.776 # +odown master mymaster 192.168.1.101 6379 #quorum 3&#x2F;2</span><br><span class="line">3031:X 17 Apr 2020 21:02:57.776 # Next failover delay: I will not start a failover before Fri Apr 17 21:08:57 2020</span><br><span class="line">3031:X 17 Apr 2020 21:02:58.308 # +config-update-from sentinel a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 192.168.1.103 26379 @ mymaster 192.168.1.101 6379</span><br><span class="line">3031:X 17 Apr 2020 21:02:58.308 # +switch-master mymaster 192.168.1.101 6379 192.168.1.103 6379</span><br><span class="line">3031:X 17 Apr 2020 21:02:58.309 * +slave slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">3031:X 17 Apr 2020 21:02:58.309 * +slave slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">3031:X 17 Apr 2020 21:03:28.352 # +sdown slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.690 # +sdown master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.749 # +odown master mymaster 192.168.1.101 6379 #quorum 2&#x2F;2</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.749 # +new-epoch 1</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.749 # +try-failover master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.750 # +vote-for-leader a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 1</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.759 # 643817dcf5ba6d53a737782a75706a62df869e33 voted for a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 1</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.759 # e784d728f7a813de688ea800a88bda6aca0512ff voted for a32bc56146695d9ebcbceaff2b0b8a5339c61a5b 1</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.841 # +elected-leader master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.841 # +failover-state-select-slave master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.924 # +selected-slave slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:57.925 * +failover-state-send-slaveof-noone slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:58.001 * +failover-state-wait-promotion slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:58.266 # +promoted-slave slave 192.168.1.103:6379 192.168.1.103 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:58.266 # +failover-state-reconf-slaves master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:58.317 * +slave-reconf-sent slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:58.817 # -odown master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.292 * +slave-reconf-inprog slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.292 * +slave-reconf-done slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.347 # +failover-end master mymaster 192.168.1.101 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.347 # +switch-master mymaster 192.168.1.101 6379 192.168.1.103 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.347 * +slave slave 192.168.1.102:6379 192.168.1.102 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">2833:X 17 Apr 2020 21:02:59.347 * +slave slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br><span class="line">2833:X 17 Apr 2020 21:03:29.355 # +sdown slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 6379</span><br></pre></td></tr></table></figure>

<p>从日志可以看到大致的过程</p>
<ul>
<li>三个sentinel都发下<code>master</code>宕机了，把它的状态设置成<code>odown</code></li>
<li>开启一轮投票，选出了新的<code>master</code>为<code>192.168.1.103:6379</code></li>
<li>sentinel更新配置文件</li>
<li><code>192.168.1.103:6379</code>成为新的<code>master</code>，故障转移完成</li>
</ul>
<p>从最后几行日志可以看出，现在的<code>master</code>是<code>192.168.1.103 6379</code>，而<code>slave</code>是<code>192.168.1.102:6379</code>和<code>192.168.1.101:6379</code>，并且<code>192.168.1.101:6379</code>是<code>sdown</code>状态的<code>slave</code>。</p>
<p>Redis 的 Sentinel 中关于下线（<code>down</code>）有两个不同的概念：</p>
<ul>
<li>主观下线（Subjectively Down， 简称 <code>SDOWN</code>）指的是单个 Sentinel 实例对服务器做出的下线判断。</li>
<li>客观下线（Objectively Down， 简称 <code>ODOWN</code>）指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断， 并且通过 <code>SENTINEL is-master-down-by-addr</code> 命令互相交流之后， 得出的服务器下线判断。（一个 Sentinel 可以通过向另一个 Sentinel 发送 <code>SENTINEL is-master-down-by-addr</code> 命令来询问对方是否认为给定的服务器已下线。）</li>
</ul>
<p>查看哨兵配置文件，发现哨兵监控的节点已经成新的<code>master</code>了</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[root@localhost redis-5.0.8]&gt; cat &#x2F;etc&#x2F;redis&#x2F;sentinel.conf |grep &quot;sentinel monitor mymaster&quot;</span><br><span class="line">sentinel monitor mymaster 192.168.1.103 6379 2</span><br></pre></td></tr></table></figure>

<p>之前的<code>master</code>出故障了，但是现在故障修复了，准备重启。重新启动原来的<code>192.168.1.101（master）</code>，它会心甘情愿的成为<code>slave</code>，还是抢回<code>master</code>地位呢？</p>
<p>欲知后事如何，请听下回分解。<br>看下哨兵日志就知道，哨兵会打印如下日志</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">3031:X 17 Apr 2020 21:05:32.297 * +convert-to-slave slave 192.168.1.101:6379 192.168.1.101 6379 @ mymaster 192.168.1.103 63791</span><br></pre></td></tr></table></figure>

<p>就是把<code>192.168.1.101:6379</code>变成可用的<code>slave</code>，所以即使原来的<code>master</code>重启了，也不会去抢回<code>master</code>地位。</p>
<p>至此，基于哨兵的高可用redis集群才算搭建完成。</p>
<p>搜索Java知音公众号，回复“后端面试”，送你一份Java面试题宝典.pdf</p>
<h2 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h2><p>这里再把主从复制相关的理论总结一下。<code>slave</code>节点第一次追随<code>master</code>的时候，会发送<code>sync</code>请求同步。请求同步在Redis2.8之后由<code>psync [runId] [offset]</code>命令完成，<code>psync</code>命令既支持全量复制，也支持增量复制。Redis4.0之后，<code>psync</code>再一次进行了优化。</p>
<ul>
<li><code>runId</code>：是每个redis节点启动都会生成的唯一标识，每次redis重启后，runId也会发生变化</li>
<li><code>offset</code>：是复制的偏移量，<code>master</code>和<code>slave</code>都会记录自己和对方的复制偏移量，如果不一致，表示需要继续同步</li>
</ul>
<p>除此之外<code>master</code>节点还会维护一个缓冲队列（<code>replication backlog buffer</code>，复制积压缓冲区默认大小1M，参数<code>repl-backing-size</code>设置），当<code>slave</code>正在复制<code>master</code>时，如果出现网络异常导致命令丢失时。<code>slave</code>会向<code>master</code>要求重新发送丢失的命令数据，如果<code>master</code>的复制积压缓冲区内存将这部分数据则直接发送给<code>slave</code>，这样就可以保持主从节点复制的一致性。</p>
<p>然而redis2.8版本的<code>psync</code>还有两个问题无法解决：redis重启时触发全量复制、故障切换之后，<code>slave</code>追随新的<code>master</code>触发全量同步。</p>
<p>这两个问题在redis4.0版本的<code>psync</code>得到了解决。主要通过两个复制id（<code>master_replid</code>和<code>master_replid2</code>）来实现</p>
<p>这些信息都可以通过<code>info replication</code>命令来查询<br>这是<code>master</code>节点的信息</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">192.168.1.103:6379&gt; info replication</span><br><span class="line"># Replication</span><br><span class="line">role:master</span><br><span class="line">connected_slaves:2</span><br><span class="line">slave0:ip&#x3D;192.168.1.101,port&#x3D;6379,state&#x3D;online,offset&#x3D;98,lag&#x3D;0</span><br><span class="line">slave1:ip&#x3D;192.168.1.102,port&#x3D;6379,state&#x3D;online,offset&#x3D;98,lag&#x3D;0</span><br><span class="line">master_replid:8b1d6db7a9e63c0360ffed0ec6d3a51199f08f2b</span><br><span class="line">master_replid2:0000000000000000000000000000000000000000</span><br><span class="line">master_repl_offset:98</span><br><span class="line">second_repl_offset:-1</span><br><span class="line">repl_backlog_active:1</span><br><span class="line">repl_backlog_size:1048576</span><br><span class="line">repl_backlog_first_byte_offset:1</span><br><span class="line">repl_backlog_histlen:98</span><br></pre></td></tr></table></figure>

<p>这是<code>slave</code>节点的信息</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">192.168.1.101:6379&gt; info replication</span><br><span class="line"># Replication</span><br><span class="line">role:slave</span><br><span class="line">master_host:192.168.1.103</span><br><span class="line">master_port:6379</span><br><span class="line">master_link_status:up</span><br><span class="line">master_last_io_seconds_ago:3</span><br><span class="line">master_sync_in_progress:0</span><br><span class="line">slave_repl_offset:5334</span><br><span class="line">slave_priority:100</span><br><span class="line">slave_read_only:1</span><br><span class="line">connected_slaves:0</span><br><span class="line">master_replid:8b1d6db7a9e63c0360ffed0ec6d3a51199f08f2b</span><br><span class="line">master_replid2:0000000000000000000000000000000000000000</span><br><span class="line">master_repl_offset:5334</span><br><span class="line">second_repl_offset:-1</span><br><span class="line">repl_backlog_active:1</span><br><span class="line">repl_backlog_size:1048576</span><br><span class="line">repl_backlog_first_byte_offset:1</span><br><span class="line">repl_backlog_histlen:5334</span><br></pre></td></tr></table></figure>

<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文以穿插的方式讲解了redis主从复制的实践和部分原理，可能会导致看起来略显凌乱。之所以采用穿插的方式，是为了让读者把理论和实践关联起来，形成一个完整的知识体系，而不仅仅是零碎的知识点。</p>
<p>只关心实验的旁友可以先跳过文中理论部分，并不会影响实验效果。</p>
<h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul>
<li><a target="_blank" rel="noopener" href="http://redis.cn/topics/sentinel.html">http://redis.cn/topics/sentinel.html</a></li>
</ul>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <div class="declare">
      <ul class="post-copyright">
        <li>
          <i class="ri-copyright-line"></i>
          <strong>版权声明： </strong>
          
          本博客所有文章除特别声明外，著作权归作者所有。转载请注明出处！
          
        </li>
      </ul>
    </div>
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=http://example.com/2020/11/11/deploy/%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84Redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%9B%86%E7%BE%A4%EF%BC%8C%E4%BB%8E%E7%90%86%E8%AE%BA%E5%88%B0%E5%AE%9E%E8%B7%B5/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/deploy/" rel="tag">deploy</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2020/11/11/deploy/%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90Kafka%20%E6%A0%B8%E5%BF%83%E8%A6%81%E7%B4%A0%E5%8F%8A%E5%85%B6%E5%B8%B8%E8%A7%81%E9%83%A8%E7%BD%B2/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            全面解析Kafka 核心要素及其常见部署.md
          
        </div>
      </a>
    
    
      <a href="/2020/11/11/develop/Python%E9%87%87%E9%9B%86linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%95%B0%E6%8D%AE%E5%9C%A8Django%20Web%E7%95%8C%E9%9D%A2%E5%B1%95%E7%A4%BA/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">Python采集linux服务器数据在Django Web界面展示.md</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "",
    app_key: "",
    path: window.location.pathname,
    avatar: "monsterid",
    placeholder: "给我的文章加点评论吧~",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
     
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2020
        <i class="ri-heart-fill heart_icon"></i> TzWind
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s9.cnzz.com/z_stat.php?id=1278069914&amp;web_id=1278069914'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="Hexo"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" target="_blank" rel="noopener" href="http://www.baidu.com">百度</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/2019/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>